/** ShadowTemplate.java.

	Purpose:
		
	Description:
		
	History:
		12:49:11 PM Aug 20, 2015, Created by chunfu

Copyright (C) 2014 Potix Corporation. All Rights Reserved.
 */
package org.zkoss.zuti.zul;

import java.io.Serializable;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import org.zkoss.util.Maps;
import org.zkoss.xel.VariableResolver;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.Templates;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.ext.DynamicPropertied;
import org.zkoss.zk.ui.util.Composer;
import org.zkoss.zk.ui.util.Template;
import org.zkoss.zul.ListModel;

/**
 * A utility to let developers to apply shadow elements in Java class. It has the similar behavior with {@link ForEach}, for
 * example, {@link #setModel(ListModel)} corresponds to {@link ForEach#setItems(Object)} but only accept {@link ListModel}.
 *
 * <p>One difference is that developers can specify the template name by {@link #setTemplate(String)} or {@link #setTemplateURI(String)},
 * and it is more flexible to assign {@link CollectionTemplateResolver} which will resolve the proper {@link Template} by evaluating
 * the variable reference from model in runtime.</p>
 *
 * <p>Besides, developers must designate a boolean value, called <tt>autodrop</tt>, to indicate whether to
 * drop those rendered children or not. If true, every time developers changed template or detach from the original host,
 * ShadowTemplate will {@link ForEach#recreate()} or removed the children, otherwise, rendered children will remain on page.</p>
 * <p>After instantiating CollectionTemplate instance and above configuration, developers can trigger {@link #apply(Component)} to compose
 * the specified template with shadow host passed as parameter. Note that, the passed host should be the same one if
 * <tt>autodrop</tt> is true, or pass null to detach the original host first.</p>
 *
 * @author chunfu
 * @since 8.0.0
 */
public class CollectionTemplate implements DynamicPropertied, Serializable {

	private static final long serialVersionUID = -2153217142078824636L;

	private boolean _autodrop;
	private Component _host;
	private ForEachEx _forEach;
	private Map<String, Object> _props;
	private String _template;
	private String _templateURI; //not supported in ForEach

	private ExecutionTemplate _execTemplate;

	/**
	 * Constructor needs a boolean value to indicate whether to detached all rendered children automatically or not when
	 * template or host is changed.
	 * @param autodrop a boolean value
	 */
	public CollectionTemplate(boolean autodrop) {
		_autodrop = autodrop;
		init();
	}

	private void init() {
		_props = new LinkedHashMap<String, Object>();
		_forEach = new ForEachEx();
		_execTemplate = new ExecutionTemplate();
	}

	/**
	 * Return the current shadow host.
	 * @return host component
	 */
	public Component getShadowHost() {
		return _host;
	}

	/**
	 * Accept model to render iteratively.
	 * @param model
	 */
	public void setModel(ListModel model) {
		_forEach.setItems(model);
	}

	public ListModel getModel() {
		return (ListModel) _forEach.getItems(); //we will only store ListModel as items
	}

	/**
	 * Template resolver will resolve the proper {@link Template} by evaluating the variable reference from model.
	 * @param templateResolver
	 */
	public void setTemplateResolver(CollectionTemplateResolver templateResolver) {
		_execTemplate.setTemplateResolver(templateResolver);
	}

	/**
	 * Returns the template name
	 */
	public String getTemplate() {
		return _template;
	}

	/**
	 * Sets the template name to apply.
	 * <p>One cannot set both template and template URI at the same time. For example, {@link #setTemplate(String)}
	 * after {@link #setTemplateURI(String)}, then only template uri takes charge of rendering.</p>
	 *
	 * @param template the template name
	 */
	public void setTemplate(String template) {
		_template = template;
		_execTemplate.setTemplateResolver(new CollectionTemplateResolverImpl(template));
	}

	/**
	 * Sets the template uri.
	 * <p>One cannot set both template and template URI at the same time. For example, {@link #setTemplate(String)}
	 * after {@link #setTemplateURI(String)}, then only template uri takes charge of rendering.</p>
	 *
	 * @param templateURI the template URI, like {@link Apply#setTemplateURI(String)}.
	 */
	public void setTemplateURI(String templateURI) {
		_templateURI = templateURI;
		_execTemplate.setTemplateResolver(new CollectionTemplateResolverImpl(templateURI, _props));
	}

	public String getTemplateURI() {
		return _templateURI;
	}

	/**
	 * Compose the specified template with the given host.
	 * Notice that,
	 * <ul>
	 *     <li>
	 *         If autodrop(the boolean value passed when instantiation) is true, users should apply to the same host
	 *         every time, otherwise, apply null to detach the original host, and then apply to the new one.
	 *     </li>
	 *     <li>
	 *        If autodrop is false, don't have to consider this situation, users can apply to any host you want except null.
	 *     </li>
	 * </ul>
	 *
	 * @param host as known as shadow host mentioned in other shadow element
	 */
	public void apply(Component host) {
		if (_autodrop) {
			applyDropTrue(host);
		} else {
			applyDropFalse(host);
		}
	}

	private void applyDropTrue(Component host) {
		if (host == null) {
			//remove all rendered children
			Component firstInsertion = _forEach.getFirstInsertion();
			if (firstInsertion != null) {
				Component lastInsertion = _forEach.getLastInsertion();
				for (Component next = firstInsertion, end = lastInsertion.getNextSibling(); next != end;) {
					Component tmp = next.getNextSibling();
					next.detach();
					next = tmp;
				}
			}

			//detach from _host
			_forEach.mergeSubTree();
			_forEach.detach();
			_host = null;
			return;
		}
		if (_host != null) { //has set host before
			if (_host != host)
				throw new UiException("The shadow element cannot change its host, if existed. [" + this
						+ "], please apply with null first!.");
		} else { //set for the first time
			_host = host;
			_forEach.setShadowHost(_host, null);
		}

		if (_forEach.getAfterCompose()) {
			_forEach.recreate();
		} else {
			_forEach.afterCompose();
		}
	}

	private void applyDropFalse(Component host) {
		if (host == null) {
			throw new UiException("The shadow host cannot be null. [" + this + "].");
		}
		_host = host;
		_forEach.setShadowHost(_host, null);
		if (_forEach.getAfterCompose()) {
			_forEach.recreate();
		} else {
			_forEach.afterCompose();
		}
		_forEach.mergeSubTree();
		_forEach.detach();
	}

	//-- DynamicPropertied --//
	public boolean hasDynamicProperty(String name) {
		return _props.containsKey(name);
	}

	public Object getDynamicProperty(String name) {
		return _props.get(name);
	}

	public Map<String, Object> getDynamicProperties() {
		return _props;
	}

	public void setDynamicProperty(String name, Object value) throws WrongValueException {
		_props.put(name, value);
	}

	/**
	 * internal implementation of ForEach
	 */
	private class ForEachEx extends ForEach {
		@Override
		public void detach() {
			//if autodrop = false, compose will remove the previous listener because forEach instance is the same one
			if (_autodrop) {
				((ListModel) getItems()).removeListDataListener(_dataListener);
			}
			super.detach();
		}

		@Override
		public Template getTemplate(String name) {
			//ZK-3110 name must be empty String, if name has given, return super.getTemplate(name)
			if (name != null && name.length() > 0) return super.getTemplate(name);
			return _execTemplate;
		}

		//in order to support dynamic properties
		public Object resolveVariable(Component child, String name, boolean recurse) {
			Object result = _props.get(name);
			if (result == null)
				return super.resolveVariable(child, name, recurse);
			else
				return result;
		}

		//allow more access
		public void mergeSubTree() {
			super.mergeSubTree();
		}

		public boolean getAfterCompose() {
			return _afterComposed;
		}
	}

	/**
	 * A proxy {@link Template} implementation which uses {@link CollectionTemplateResolver} for creating components.
	 * The default implementation({@link org.zkoss.zuti.zul.CollectionTemplate.CollectionTemplateResolverImpl}
	 * will return template which creates components by {@link Execution#createComponents(String, Component, Component, VariableResolver, Map)}
	 * when template uri is given or is found recursively by template name like what {@link Apply} did.
	 */
	private class ExecutionTemplate implements Template {
		CollectionTemplateResolver _templateResolver;

		public ExecutionTemplate() {
		}

		public ExecutionTemplate(CollectionTemplateResolver templateResolver) {
			_templateResolver = templateResolver;
		}

		public void setTemplateResolver(CollectionTemplateResolver templateResolver) {
			_templateResolver = templateResolver;
		}

		public CollectionTemplateResolver getTemplateResolver() {
			return _templateResolver;
		}

		public Component[] create(Component parent, Component insertBefore, VariableResolver resolver,
				Composer composer) {
			Template resolvedTemplate = _templateResolver.resolve(resolver.resolveVariable(_forEach.getVar()));
			if (resolvedTemplate != null)
				return resolvedTemplate.create(parent, insertBefore, resolver, composer);
			else
				return null;
		}

		public Map<String, Object> getParameters() {
			return null;
		}
	}

	/**
	 * Default implementation of {@link CollectionTemplateResolver}.
	 * Return two kinds of templates:
	 * 1. if there is template uri -> create components using  {@link Execution#createComponents(String, Component, Component, VariableResolver, Map)}
	 * 2. if there is template name -> look up template recursively
	 */
	private class CollectionTemplateResolverImpl implements CollectionTemplateResolver {
		private String _templateName;
		private String _uri;
		private Map _arg;
		Map<String, Template> _templateCache = new HashMap<String, Template>(4);

		public CollectionTemplateResolverImpl(String templateName) {
			_templateName = templateName;
		}

		public CollectionTemplateResolverImpl(String uri, Map<?, ?> arg) {
			_uri = uri;
			_arg = arg;
		}

		/**
		 * Refer to {@link Apply#lookupTemplate}
		 */
		private Template lookupTemplate(Component comp, ForEachEx base, String name) {
			Template templateFromeCache = _templateCache.get(name);
			if (templateFromeCache != null)
				return templateFromeCache;

			Template t = Templates.lookup(comp, base, name, null, true);
			if (t != null)
				_templateCache.put(name, t);
			return t;
		}

		private Template getTemplateURI() {
			if (_uri != null && _uri.length() > 0) {
				String uri = _uri;
				final int queryStart = uri.indexOf('?');
				String queryString = null;
				if (queryStart >= 0) {
					queryString = uri.substring(queryStart + 1);
					uri = uri.substring(0, queryStart);
				}
				Map<String, String> map = (Map<String, String>) Maps.parse(null, queryString, '&', '=', false);
				Map<Object, Object> arg = new HashMap<Object, Object>();
				arg.putAll(_arg);
				arg.putAll(map);
				return new Template() {
					public Component[] create(Component parent, Component insertBefore, VariableResolver resolver,
							Composer composer) {
						final Execution exec = Executions.getCurrent();
						return exec.createComponents(_uri, parent, insertBefore, resolver, _arg);
					}

					public Map<String, Object> getParameters() {
						return null;
					}
				};
			}
			return null;
		}

		public Template resolve(Object o) {
			//don't care the arguments
			if (_templateName != null && _templateName.length() > 0)
				return lookupTemplate(_forEach, _forEach, _templateName);
			else
				return getTemplateURI();
		}
	}
}
